/*NOTICE

This software was produced for the U. S. Government
under Contract No. FA8721-09-C-0002, and is
subject to the Rights in Noncommercial Computer Software
and Noncommercial Computer Software Documentation Clause
(DFARS) 252.227-7014 (JUN 1995)

(c) 2009 The MITRE Corporation. All Rights Reserved.
*/
package org.mitre.xmltransform.wsdl;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.wsdl.Definition;
import javax.wsdl.Operation;
import javax.wsdl.Part;
import javax.wsdl.PortType;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.xml.namespace.QName;

import org.mitre.xmltransform.xsd.XsdInputDocument2;
import org.w3c.dom.Element;

import com.ibm.wsdl.extensions.schema.SchemaImpl;
import com.sun.xml.xsom.XSAttGroupDecl;
import com.sun.xml.xsom.XSAttributeUse;
import com.sun.xml.xsom.XSComplexType;
import com.sun.xml.xsom.XSElementDecl;
import com.sun.xml.xsom.XSFacet;
import com.sun.xml.xsom.XSModelGroup;
import com.sun.xml.xsom.XSParticle;
import com.sun.xml.xsom.XSRestrictionSimpleType;
import com.sun.xml.xsom.XSSimpleType;
import com.sun.xml.xsom.XSType;

public class WsdlInputDocument {
	public WsdlInputDocument(File pInputFile) {
		mInputFile = pInputFile;
		mLoaded = false;
	}
	
	private void loadWsdl() {
		try
		{
			WSDLFactory wsdlFactory = WSDLFactory.newInstance();
			WSDLReader wsdlReader = wsdlFactory.newWSDLReader();
	
			wsdlReader.setFeature("javax.wsdl.verbose",false);
			wsdlReader.setFeature("javax.wsdl.importDocuments",true);
	
			mDefinition = wsdlReader.readWSDL(mInputFile.getAbsolutePath());
			if (mDefinition == null) {
				System.err.println("definition element is null");
				return;
			}
			
			String keyNamespace;
			Map<String,String> namespaces;
			Element[] schemaElements;
			List<ExtensibilityElement> schemaExtElems = findExtensibilityElement("schema");

			mWsdlTypes = new XsdInputDocument2[schemaExtElems.size()];
			schemaElements = new Element[schemaExtElems.size()];
			
			mLoaded = true;
			int i = 0;
			for (ExtensibilityElement schemaExtElem : schemaExtElems) {
				schemaElements[i] = ((SchemaImpl)schemaExtElem).getElement();
				i++;
			}
				
			for (i = 0; i < schemaElements.length; i++) {
				namespaces = (Map<String,String>)mDefinition.getNamespaces();
				for (String key : namespaces.keySet()) {
					//System.out.println("key = " + key);
					if (key.length() > 0 && !schemaElements[i].hasAttribute("xmlns:" + key)) {
						keyNamespace = namespaces.get(key);
						if (!keyNamespace.equals("http://schemas.xmlsoap.org/wsdl/") 
								&& !keyNamespace.equals("http://www.w3.org/2001/XMLSchema"))
						{
							//System.out.println("adding xmlns:" + key + " --> " + keyNamespace);
							schemaElements[i].setAttribute("xmlns:" + key, keyNamespace);
						}
					}
				}
				mWsdlTypes[i] = new XsdInputDocument2(mInputFile.getAbsolutePath(), schemaElements[i], schemaElements);
				mLoaded &= mWsdlTypes[i].isValid();
			}
			
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public class ValueSpec {
		public ValueSpec(String pPath, String pType) {
			mPath = pPath;
			mType = pType;
		}
		public String getPath() {
			return mPath;
		}
		public String getType() {
			return mType;
		}
		private String mPath;
		private String mType;
	}
	
	public List<ValueSpec> getOperationInputs(String pNamespace, String pPortType, String pOperationName) {
		Operation op = getOperation(pNamespace, pPortType, pOperationName);
		if (op == null) {
			System.err.println("Operation " + pOperationName + " does not exist in " + pNamespace + ":" + pPortType + ".");
			return null;
		}
		Part part;
		//String inPartName;
		QName elementType;
		XSType type;
		List<ValueSpec> result = new ArrayList<ValueSpec>();
		
		Map inputParts = op.getInput().getMessage().getParts();
		Iterator<Part> partsIter = inputParts.values().iterator();
		while (partsIter.hasNext()) {
			part = partsIter.next();
			elementType = part.getElementName();
			type = getType(elementType);
			buildValueSpecs(type, "", result);
		}
		return result;
	}
	
	public List<ValueSpec> getOperationOutputs(String pNamespace, String pPortType, String pOperationName) {
		Operation op = getOperation(pNamespace, pPortType, pOperationName);
		if (op == null) {
			System.err.println("Operation " + pOperationName + " does not exist in " + pNamespace + ":" + pPortType + ".");
			return null;
		}
		Part part;
		List<ValueSpec> result = new ArrayList<ValueSpec>();
		QName elementType;
		XSType type;

		Map<?,?> outputParts = op.getOutput().getMessage().getParts();
		Iterator<Part> partsIter = (Iterator<Part>)outputParts.values().iterator();
		while (partsIter.hasNext()) {
			part = partsIter.next();
			elementType = part.getElementName();
			type = getType(elementType);
			buildValueSpecs(type, "", result);
		}
		return result;
	}
	
	private void buildValueSpecsInModelGroup(XSModelGroup pModelGroup, String pBasePath, List<ValueSpec> pResultList, boolean pIsChoice) {
		String itemName;
		int minOccurs, maxOccurs;
		XSParticle[] particles = pModelGroup.getChildren();
		XSElementDecl elemDecl;
		for (int i = 0; i < particles.length; i++) {
			minOccurs = particles[i].getMinOccurs();
			maxOccurs = particles[i].getMaxOccurs();
			if (particles[i].asParticle().getTerm().isElementDecl()) {
				elemDecl = particles[i].asParticle().getTerm().asElementDecl();
				itemName = elemDecl.getName();
				if (pIsChoice) {
					itemName = "(C)" + itemName;
				}
				if (minOccurs != 1 || maxOccurs != 1) {
					itemName += "[" + minOccurs + ":" + ((maxOccurs == -1) ? "*" : maxOccurs) + "]";
				}
				buildValueSpecs(elemDecl.getType(), 
						appendToPath(pBasePath, itemName), 
						pResultList);
			} else if (particles[i].asParticle().getTerm().isModelGroup()) {
				buildValueSpecsInModelGroup(particles[i].asParticle().getTerm().asModelGroup(), pBasePath, pResultList, 
						particles[i].asParticle().getTerm().asModelGroup().getCompositor().name().equals("CHOICE"));
			} else {
				System.out.println("unsupported particle term: " + particles[i].asParticle().getTerm().getClass().getName());
			}
		}
	}
	
	private void buildValueSpecs(XSType pType, String pBasePath, List<ValueSpec> pResultList) {
		XSType baseType = pType.getBaseType();
		if (baseType != null && !baseType.getTargetNamespace().equals("http://www.w3.org/2001/XMLSchema")) {
			buildValueSpecs(baseType, pBasePath, pResultList);
			if (pType.isSimpleType()) return;
		}
		if (pType.getName() != null && pType.getName().equals("anyType") && pType.getTargetNamespace().equals("http://www.w3.org/2001/XMLSchema")) {
			ValueSpec vs = createValueSpec(pType, pBasePath);
			pResultList.add(vs);			
		} else if (pType.isComplexType()) {
			XSComplexType complexType = pType.asComplexType();
			if (complexType.getContentType().asParticle() != null) {
				XSModelGroup modelGroup = complexType.getContentType().asParticle().getTerm().asModelGroup();
				boolean isChoice = modelGroup.getCompositor().name().equals("CHOICE");
				buildValueSpecsInModelGroup(modelGroup, pBasePath, pResultList, isChoice);
			}
			XSAttributeUse attrib;
			Collection<?> attribGroups = complexType.getAttGroups();
			XSAttGroupDecl attribGroup;
			Iterator<?> attribGroupIter = null;
			if (attribGroups != null) {
				attribGroupIter = attribGroups.iterator();
			}

			Collection<?> attributeUses = complexType.getAttributeUses();
			String attribName;
			do {
				for (Object obj : attributeUses) {
					attrib = (XSAttributeUse)obj;
					attribName = attrib.getDecl().getName();
					if (!attrib.isRequired()) {
						attribName += "[0..1]";
					}
					buildValueSpecs(attrib.getDecl().getType(), 
							appendToPath(pBasePath, attribName), 
							pResultList);
				}
				if (attribGroupIter != null && attribGroupIter.hasNext()) {
					attribGroup = (XSAttGroupDecl)attribGroupIter.next();
					attributeUses = attribGroup.getAttributeUses();
				} else {
					break;
				}
			} while (true);
			
		} else if (pType.isSimpleType()) {
			ValueSpec vs = createValueSpec(pType, pBasePath);
			pResultList.add(vs);
		}
	}
	
	private ValueSpec createValueSpec(XSType pType, String pBasePath) {
		ValueSpec vs;
		if (pType.getTargetNamespace().equals("http://www.w3.org/2001/XMLSchema")) {
			if (pType.getName().equals("anyType")) {
				vs = new ValueSpec(pBasePath, "anyType");
			} else {
				vs = new ValueSpec(pBasePath, pType.asSimpleType().getName());
			}
		} else {
			String minInclusive = null, maxInclusive = null;
			String minExclusive = null, maxExclusive = null;
			String minLength = null, maxLength = null;
			XSSimpleType simpleType = pType.asSimpleType();
			XSRestrictionSimpleType restriction = simpleType.asRestriction();
			StringBuffer typeSpec = new StringBuffer(restriction.getBaseType().getName());
			
			XSFacet facet;
			int enumCount = 0;
			Collection<?> facets = restriction.getDeclaredFacets();
			for (Object obj : facets) {
				facet = (XSFacet)obj;
				if (facet.getName().equals("maxLength")) {
					maxLength = facet.getValue().toString();
				} else if (facet.getName().equals("minLength")) {
					minLength = facet.getValue().toString();
				} else if (facet.getName().equals("enumeration")) {
					if (enumCount == 0) {
						typeSpec.append("[");
					} else {
						typeSpec.append(" | ");
					}
					typeSpec.append(facet.getValue().toString());
					enumCount++;
				} else if (facet.getName().equals("minExclusive")) {
					minExclusive = facet.getValue().toString();
				} else if (facet.getName().equals("maxExclusive")) {
					maxExclusive = facet.getValue().toString();
				} else if (facet.getName().equals("maxInclusive")) {
					maxInclusive = facet.getValue().toString();
				} else if (facet.getName().equals("minInclusive")) {
					minInclusive = facet.getValue().toString();
				} else if (facet.getName().equals("minLength")) {
					minLength = facet.getValue().toString();
				} else if (facet.getName().equals("maxLength")) {
					maxLength = facet.getValue().toString();
				} else if (facet.getName().equals("fractionDigits")) {
					typeSpec.append(".");
					typeSpec.append(facet.getValue().toString());
				} else if (facet.getName().equals("pattern")) {
					typeSpec.append("{");
					typeSpec.append(facet.getValue().toString());
					typeSpec.append("}");
				} else if (facet.getName().equals("length")) {
					// ignore
				} else {
					System.out.println("unsupported facet = " + facet.getName());
				}
			}
			if (enumCount > 0) {
				typeSpec.append("]");
			}
			if (minInclusive != null || maxInclusive != null || minExclusive != null || maxExclusive != null) {
				if (minInclusive != null) {
					typeSpec.append("[");
					typeSpec.append(minInclusive);
				}
				if (minExclusive != null) {
					typeSpec.append("(");
					typeSpec.append(minExclusive);
				}
				typeSpec.append("...");
				if (maxInclusive != null) {
					typeSpec.append(maxInclusive);
					typeSpec.append("]");
				}
				if (maxExclusive != null) {
					typeSpec.append(maxExclusive);
					typeSpec.append(")");
				}
			}
			if (minLength != null || maxLength != null) {
				typeSpec.append("[");
				if (minLength != null) {
					typeSpec.append(minLength);
				}
				typeSpec.append(":");
				if (maxLength != null) {
					typeSpec.append(maxLength);
				}
				typeSpec.append("]");
			}
			vs = new ValueSpec(pBasePath, typeSpec.toString());
		}
		return vs;
	}
	
	private String appendToPath(String pBasePath, String pItemToAdd) {
		if (pBasePath.length() == 0) {
			return pItemToAdd;
		} else {
			return pBasePath + ATTRIBUTE_DELIMITER + pItemToAdd;
		}
	}
	
	private Operation getOperation(String pNamespace, String pPortType, String pOperationName) {
		if (!mLoaded) loadWsdl();
		if (!mLoaded) throw new IllegalArgumentException("Unable to load WSDL from " + mInputFile.getAbsolutePath());
		
		PortType portType = mDefinition.getPortType(new QName(pNamespace, pPortType));
		if (portType == null) throw new IllegalArgumentException("Port type '" + pPortType + "' in namespace '" + pNamespace + "' not found.");
		return portType.getOperation(pOperationName, null, null);
	}
	
	private List<ExtensibilityElement> findExtensibilityElement(String pName) {
		List<ExtensibilityElement> result = new ArrayList<ExtensibilityElement>();
		List<ExtensibilityElement> list = (List<ExtensibilityElement>)
				mDefinition.getTypes().getExtensibilityElements();
		for (ExtensibilityElement elem : list) {
			if (elem.getElementType().getLocalPart().equalsIgnoreCase(pName)) {
				result.add(elem);
			}
		}
		return result;
	}
	
	private XSType getType(QName pElementType) {
		String namespace = pElementType.getNamespaceURI();
		String localName = pElementType.getLocalPart();
		
		XSElementDecl elem = null;
		for (int i = 0; i < mWsdlTypes.length; i++) {
			elem = mWsdlTypes[i].getElement(namespace, localName);
			if (elem != null) break;
		}
		if (elem == null) {
			System.err.println("Element {" + pElementType.getNamespaceURI() + "}:" + pElementType.getLocalPart() + " not found.");
			return null;
		}
		return elem.getType();
	}
	
	private static final String ATTRIBUTE_DELIMITER = ".";
	
	private boolean mLoaded;
	private Definition mDefinition;
	private XsdInputDocument2[] mWsdlTypes;
	private File mInputFile;
}
